{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3bf28f9e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "93ac45ce",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# .env 파일에 LANGCHAIN_API_KEY를 입력합니다.\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH01-Basic\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2550920c-09d8-48b3-be2f-b36362c37989",
   "metadata": {},
   "source": [
    "## ChatOpenAI\n",
    "\n",
    "OpenAI 사의 채팅 전용 Large Language Model(llm) 입니다.\n",
    "\n",
    "객체를 생성할 때 다음을 옵션 값을 지정할 수 있습니다. 옵션에 대한 상세 설명은 다음과 같습니다.\n",
    "\n",
    "`temperature`\n",
    "\n",
    "- 사용할 샘플링 온도는 0과 2 사이에서 선택합니다. 0.8과 같은 높은 값은 출력을 더 무작위하게 만들고, 0.2와 같은 낮은 값은 출력을 더 집중되고 결정론적으로 만듭니다.\n",
    "\n",
    "`max_tokens`\n",
    "\n",
    "- 채팅 완성에서 생성할 토큰의 최대 개수입니다.\n",
    "\n",
    "`model_name`: 적용 가능한 모델 리스트\n",
    "\n",
    "| 모델 계열       | 모델명 (API Name) | 입력       | 컨텍스트 윈도우  | 최대 출력 토큰 | 지식 마감일 (Cutoff) | 가격                  |\n",
    "| :---------- | :------------- | :------- | :-------- | :------- | :-------------- | :------------------------------ |\n",
    "| **GPT-5**   | `gpt-5`        | 텍스트, 이미지 | 400,000   | 128,000  | 2024년 9월 30일    | **입력:** $1.25<br>**출력:** $10.00 |\n",
    "|             | `gpt-5-mini`   | 텍스트, 이미지 | 400,000   | 128,000  | 2024년 5월 31일    | **입력:** $0.25<br>**출력:** $2.00  |\n",
    "|             | `gpt-5-nano`   | 텍스트, 이미지 | 400,000   | 128,000  | 2024년 5월 31일    | **입력:** $0.05<br>**출력:** $0.40  |\n",
    "| **GPT-4.1** | `gpt-4.1`      | 텍스트, 이미지 | 1,047,576 | 32,768   | 2024년 6월 1일     | **입력:** $2.00<br>**출력:** $8.00  |\n",
    "|             | `gpt-4.1-mini` | 텍스트, 이미지 | 1,047,576 | 32,768   | 2024년 6월 1일     | **입력:** $0.40<br>**출력:** $1.60  |\n",
    "|             | `gpt-4.1-nano` | 텍스트, 이미지 | 1,047,576 | 32,768   | 2024년 6월 1일     | **입력:** $0.10<br>**출력:** $0.40  |\n",
    "| **GPT-4o**  | `gpt-4o`       | 텍스트, 이미지 | 128,000   | 16,384   | 2023년 10월 1일    | **입력:** $2.50<br>**출력:** $10.00 |\n",
    "|             | `gpt-4o-mini`  | 텍스트, 이미지 | 128,000   | 16,384   | 2023년 10월 1일    | **입력:** $0.15<br>**출력:** $0.60  |\n",
    "\n",
    "![gpt-models.png](./images/gpt-models3-202508.png)\n",
    "\n",
    "- 링크: https://platform.openai.com/docs/models"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5fc161c2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# 객체 생성\n",
    "llm = ChatOpenAI(\n",
    "    temperature=0.1,  # 창의성 (0.0 ~ 2.0)\n",
    "    model_name=\"gpt-4.1-nano\",  # 모델명\n",
    ")\n",
    "\n",
    "# 질의내용\n",
    "question = \"대한민국의 수도는 어디인가요?\"\n",
    "\n",
    "# 질의\n",
    "print(f\"[답변]: {llm.invoke(question)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2ef2647",
   "metadata": {},
   "source": [
    "### 답변의 형식(AI Message)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2af58a9a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질의내용\n",
    "question = \"대한민국의 수도는 어디인가요?\"\n",
    "\n",
    "# 질의\n",
    "response = llm.invoke(question)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24ecdeb9",
   "metadata": {},
   "outputs": [],
   "source": [
    "response"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ffd49c11",
   "metadata": {},
   "outputs": [],
   "source": [
    "response.content"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4df69214",
   "metadata": {},
   "outputs": [],
   "source": [
    "response.response_metadata"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "74c4a51a",
   "metadata": {},
   "source": [
    "### LogProb 활성화\n",
    "\n",
    "주어진 텍스트에 대한 모델의 **토큰 확률의 로그 값** 을 의미합니다. 토큰이란 문장을 구성하는 개별 단어나 문자 등의 요소를 의미하고, 확률은 **모델이 그 토큰을 예측할 확률**을 나타냅니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "fe733438",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 객체 생성\n",
    "llm_with_logprob = ChatOpenAI(\n",
    "    temperature=0.1,  # 창의성 (0.0 ~ 2.0)\n",
    "    max_tokens=2048,  # 최대 토큰수\n",
    "    model_name=\"gpt-4.1-nano\",  # 모델명\n",
    ").bind(logprobs=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8ae2d627",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 질의내용\n",
    "question = \"대한민국의 수도는 어디인가요?\"\n",
    "\n",
    "# 질의\n",
    "response = llm_with_logprob.invoke(question)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e6b0b9f7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 결과 출력\n",
    "response.response_metadata"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8aec3e6",
   "metadata": {},
   "source": [
    "### 스트리밍 출력\n",
    "\n",
    "스트리밍 옵션은 질의에 대한 답변을 실시간으로 받을 때 유용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "1bbc5d03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 스트림 방식으로 질의\n",
    "# answer 에 스트리밍 답변의 결과를 받습니다.\n",
    "answer = llm.stream(\"대한민국의 아름다운 관광지 10곳과 주소를 알려주세요!\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "87a90e66",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 스트리밍 방식으로 각 토큰을 출력합니다. (실시간 출력)\n",
    "for token in answer:\n",
    "    print(token.content, end=\"\", flush=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6f079b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.messages import stream_response\n",
    "\n",
    "# 스트림 방식으로 질의\n",
    "# answer 에 스트리밍 답변의 결과를 받습니다.\n",
    "answer = llm.stream(\"대한민국의 아름다운 관광지 10곳과 주소를 알려주세요!\")\n",
    "stream_response(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e0e35de",
   "metadata": {},
   "source": [
    "## 프롬프트 캐싱\n",
    "\n",
    "- 참고 링크: https://platform.openai.com/docs/guides/prompt-caching"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a06ff75",
   "metadata": {},
   "source": [
    "프롬프트 캐싱 기능을 활용하면 반복하여 동일하게 입력으로 들어가는 토큰에 대한 비용을 아낄 수 있습니다.\n",
    "\n",
    "다만, 캐싱에 활용할 토큰은 고정된 PREFIX 를 주는 것이 권장됩니다.\n",
    "\n",
    "아래의 예시에서는 `<PROMPT_CACHING>` 부분에 고정된 토큰을 주어 캐싱을 활용하는 방법을 설명합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b8b23cdc",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.messages import stream_response\n",
    "\n",
    "very_long_prompt = \"\"\"\n",
    "당신은 매우 친절한 AI 어시스턴트 입니다. \n",
    "당신의 임무는 주어진 질문에 대해 친절하게 답변하는 것입니다.\n",
    "아래는 사용자의 질문에 답변할 때 참고할 수 있는 정보입니다.\n",
    "주어진 정보를 참고하여 답변해 주세요.\n",
    "\n",
    "<WANT_TO_CACHE_HERE>\n",
    "#참고:\n",
    "**Prompt Caching**\n",
    "Model prompts often contain repetitive content, like system prompts and common instructions. OpenAI routes API requests to servers that recently processed the same prompt, making it cheaper and faster than processing a prompt from scratch. This can reduce latency by up to 80% and cost by 50% for long prompts. Prompt Caching works automatically on all your API requests (no code changes required) and has no additional fees associated with it.\n",
    "\n",
    "Prompt Caching is enabled for the following models:\n",
    "\n",
    "gpt-4.1 (excludes gpt-4.1-2024-05-13 and chatgpt-4.1-latest)\n",
    "gpt-4.1-mini\n",
    "o1-preview\n",
    "o1-mini\n",
    "This guide describes how prompt caching works in detail, so that you can optimize your prompts for lower latency and cost.\n",
    "\n",
    "Structuring prompts\n",
    "Cache hits are only possible for exact prefix matches within a prompt. To realize caching benefits, place static content like instructions and examples at the beginning of your prompt, and put variable content, such as user-specific information, at the end. This also applies to images and tools, which must be identical between requests.\n",
    "\n",
    "How it works\n",
    "Caching is enabled automatically for prompts that are 1024 tokens or longer. When you make an API request, the following steps occur:\n",
    "\n",
    "Cache Lookup: The system checks if the initial portion (prefix) of your prompt is stored in the cache.\n",
    "Cache Hit: If a matching prefix is found, the system uses the cached result. This significantly decreases latency and reduces costs.\n",
    "Cache Miss: If no matching prefix is found, the system processes your full prompt. After processing, the prefix of your prompt is cached for future requests.\n",
    "Cached prefixes generally remain active for 5 to 10 minutes of inactivity. However, during off-peak periods, caches may persist for up to one hour.\n",
    "\n",
    "Requirements\n",
    "Caching is available for prompts containing 1024 tokens or more, with cache hits occurring in increments of 128 tokens. Therefore, the number of cached tokens in a request will always fall within the following sequence: 1024, 1152, 1280, 1408, and so on, depending on the prompt's length.\n",
    "\n",
    "All requests, including those with fewer than 1024 tokens, will display a cached_tokens field of the usage.prompt_tokens_details chat completions object indicating how many of the prompt tokens were a cache hit. For requests under 1024 tokens, cached_tokens will be zero.\n",
    "\n",
    "What can be cached\n",
    "Messages: The complete messages array, encompassing system, user, and assistant interactions.\n",
    "Images: Images included in user messages, either as links or as base64-encoded data, as well as multiple images can be sent. Ensure the detail parameter is set identically, as it impacts image tokenization.\n",
    "Tool use: Both the messages array and the list of available tools can be cached, contributing to the minimum 1024 token requirement.\n",
    "Structured outputs: The structured output schema serves as a prefix to the system message and can be cached.\n",
    "Best practices\n",
    "Structure prompts with static or repeated content at the beginning and dynamic content at the end.\n",
    "Monitor metrics such as cache hit rates, latency, and the percentage of tokens cached to optimize your prompt and caching strategy.\n",
    "To increase cache hits, use longer prompts and make API requests during off-peak hours, as cache evictions are more frequent during peak times.\n",
    "Prompts that haven't been used recently are automatically removed from the cache. To minimize evictions, maintain a consistent stream of requests with the same prompt prefix.\n",
    "Frequently asked questions\n",
    "How is data privacy maintained for caches?\n",
    "\n",
    "Prompt caches are not shared between organizations. Only members of the same organization can access caches of identical prompts.\n",
    "\n",
    "Does Prompt Caching affect output token generation or the final response of the API?\n",
    "\n",
    "Prompt Caching does not influence the generation of output tokens or the final response provided by the API. Regardless of whether caching is used, the output generated will be identical. This is because only the prompt itself is cached, while the actual response is computed anew each time based on the cached prompt. \n",
    "\n",
    "Is there a way to manually clear the cache?\n",
    "\n",
    "Manual cache clearing is not currently available. Prompts that have not been encountered recently are automatically cleared from the cache. Typical cache evictions occur after 5-10 minutes of inactivity, though sometimes lasting up to a maximum of one hour during off-peak periods.\n",
    "\n",
    "Will I be expected to pay extra for writing to Prompt Caching?\n",
    "\n",
    "No. Caching happens automatically, with no explicit action needed or extra cost paid to use the caching feature.\n",
    "\n",
    "Do cached prompts contribute to TPM rate limits?\n",
    "\n",
    "Yes, as caching does not affect rate limits.\n",
    "\n",
    "Is discounting for Prompt Caching available on Scale Tier and the Batch API?\n",
    "\n",
    "Discounting for Prompt Caching is not available on the Batch API but is available on Scale Tier. With Scale Tier, any tokens that are spilled over to the shared API will also be eligible for caching.\n",
    "\n",
    "Does Prompt Caching work on Zero Data Retention requests?\n",
    "\n",
    "Yes, Prompt Caching is compliant with existing Zero Data Retention policies.\n",
    "</WANT_TO_CACHE_HERE>\n",
    "\n",
    "#Question:\n",
    "{}\n",
    "\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5a62903d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.callbacks import get_openai_callback\n",
    "\n",
    "with get_openai_callback() as cb:\n",
    "    # 답변 요청\n",
    "    answer = llm.invoke(\n",
    "        very_long_prompt.format(\"프롬프트 캐싱 기능에 대해 2문장으로 설명하세요\")\n",
    "    )\n",
    "    print(cb)\n",
    "    # 캐싱된 토큰 출력\n",
    "    cached_tokens = answer.response_metadata[\"token_usage\"][\"prompt_tokens_details\"][\n",
    "        \"cached_tokens\"\n",
    "    ]\n",
    "    print(f\"캐싱된 토큰: {cached_tokens}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4babc2f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "with get_openai_callback() as cb:\n",
    "    # 답변 요청\n",
    "    answer = llm.invoke(\n",
    "        very_long_prompt.format(\"프롬프트 캐싱 기능에 대해 2문장으로 설명하세요\")\n",
    "    )\n",
    "    print(cb)\n",
    "    # 캐싱된 토큰 출력\n",
    "    cached_tokens = answer.response_metadata[\"token_usage\"][\"prompt_tokens_details\"][\n",
    "        \"cached_tokens\"\n",
    "    ]\n",
    "    print(f\"캐싱된 토큰: {cached_tokens}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e95ce8b",
   "metadata": {},
   "source": [
    "## 멀티모달 모델(이미지 인식)\n",
    "\n",
    "멀티모달은 여러 가지 형태의 정보(모달)를 통합하여 처리하는 기술이나 접근 방식을 의미합니다. 이는 다음과 같은 다양한 데이터 유형을 포함할 수 있습니다.\n",
    "\n",
    "- 텍스트: 문서, 책, 웹 페이지 등의 글자로 된 정보\n",
    "- 이미지: 사진, 그래픽, 그림 등 시각적 정보\n",
    "- 오디오: 음성, 음악, 소리 효과 등의 청각적 정보\n",
    "- 비디오: 동영상 클립, 실시간 스트리밍 등 시각적 및 청각적 정보의 결합\n",
    "\n",
    "`gpt-4.1` 모델은 이미지 인식 기능(Vision) 이 추가되어 있는 모델입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "a859058d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.models import MultiModal\n",
    "from langchain_teddynote.messages import stream_response\n",
    "\n",
    "# 객체 생성\n",
    "llm = ChatOpenAI(\n",
    "    temperature=0.1,  # 창의성 (0.0 ~ 2.0)\n",
    "    model_name=\"gpt-4.1-nano\",  # 모델명\n",
    ")\n",
    "\n",
    "# 멀티모달 객체 생성\n",
    "multimodal_llm = MultiModal(llm)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c16ef3c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 샘플 이미지 주소(웹사이트로 부터 바로 인식)\n",
    "IMAGE_URL = \"https://t3.ftcdn.net/jpg/03/77/33/96/360_F_377339633_Rtv9I77sSmSNcev8bEcnVxTHrXB4nRJ5.jpg\"\n",
    "\n",
    "# 이미지 파일로 부터 질의\n",
    "answer = multimodal_llm.stream(IMAGE_URL)\n",
    "# 스트리밍 방식으로 각 토큰을 출력합니다. (실시간 출력)\n",
    "stream_response(answer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "006ec2bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 로컬 PC 에 저장되어 있는 이미지의 경로 입력\n",
    "IMAGE_PATH_FROM_FILE = \"./images/sample-image.png\"\n",
    "\n",
    "# 이미지 파일로 부터 질의(스트림 방식)\n",
    "answer = multimodal_llm.stream(IMAGE_PATH_FROM_FILE)\n",
    "# 스트리밍 방식으로 각 토큰을 출력합니다. (실시간 출력)\n",
    "stream_response(answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7b5fc02",
   "metadata": {},
   "source": [
    "## System, User 프롬프트 수정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "4be092af",
   "metadata": {},
   "outputs": [],
   "source": [
    "system_prompt = \"\"\"당신은 표(재무제표) 를 해석하는 금융 AI 어시스턴트 입니다. \n",
    "당신의 임무는 주어진 테이블 형식의 재무제표를 바탕으로 흥미로운 사실을 정리하여 친절하게 답변하는 것입니다.\"\"\"\n",
    "\n",
    "user_prompt = \"\"\"당신에게 주어진 표는 회사의 재무제표 입니다. 흥미로운 사실을 정리하여 답변하세요.\"\"\"\n",
    "\n",
    "# 멀티모달 객체 생성\n",
    "multimodal_llm_with_prompt = MultiModal(\n",
    "    llm, system_prompt=system_prompt, user_prompt=user_prompt\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "51735d05",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 로컬 PC 에 저장되어 있는 이미지의 경로 입력\n",
    "IMAGE_PATH_FROM_FILE = \"https://storage.googleapis.com/static.fastcampus.co.kr/prod/uploads/202212/080345-661/kwon-01.png\"\n",
    "\n",
    "# 이미지 파일로 부터 질의(스트림 방식)\n",
    "answer = multimodal_llm_with_prompt.stream(IMAGE_PATH_FROM_FILE)\n",
    "\n",
    "# 스트리밍 방식으로 각 토큰을 출력합니다. (실시간 출력)\n",
    "stream_response(answer)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-CdOel15G-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
